package com.google.bitcoin.core;

import java.util.Set;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Collection;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.TreeMap;
import java.util.Random;

import java.math.BigInteger;

import java.text.DecimalFormat;

import com.google.bitcoin.db.WalletDB;


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;



/** Making mushroom soup I am */
public class SendFactory
{
    private static final Logger log = LoggerFactory.getLogger(SendFactory.class);

    Random rnd=new Random();
    NetworkParameters params;

    /**
     * This is the set of from addresses that many NEVER be used
     * to pay a transaction without being explicitly asked for
     */
    Set<Address> proscribed_from_addresses;

    /**
     * Set of change addresses which may be used for change
     */
    ArrayList<Address> change_addresses;

    /** 
     * If the change amount exceeds this, split it up
     */
    BigInteger max_change_before_split=getBitIntFromBtc(20000.0);


    /**
     * After we turn this many outputs into inputs on this TX
     * we start sorting the inputs to use the ones closest to our target amount
     * to start using them more efficently and hopefully not build huge transactions.
     *
     * We don't do this immediately because we want to use up little stupid
     * outputs before going to the sort.  Otherwise, I fear we would never use 
     * the confetti and it would build up.
     */
    int inputs_before_we_get_serious = 10;


    public void setMaxChangeBeforeSplit(BigInteger max_change_before_split)
    {
        this.max_change_before_split = max_change_before_split;
    }

    boolean use_pending = true;


    /**
     * Use pending funds if needed to create transaction?
     */
    public void usePending(boolean use_pending)
    {
        this.use_pending = use_pending;
    }

    FeeFinder fee_finder = new FeeFinderCounting();

    public void setFeeFinder(FeeFinder fee_finder)
    {
        this.fee_finder = fee_finder;
    }

    WalletInterface wallet;

    boolean consolidate_on_send=true;
    int consolidate_items=3;
    BigInteger consolidate_below=getBitIntFromBtc(0.5);
    BlockChainCache block_chain_cache;

    public SendFactory(NetworkParameters params, WalletInterface wallet, BlockChainCache block_chain_cache)
    {
        this.params = params;
        this.wallet = wallet;
        this.block_chain_cache = block_chain_cache;
        setChangeAddressesKey(wallet.getKeys());
    }
    public void setChangeAddressesAddr(Iterable<Address> addresses)
    {
        ArrayList<Address> lst = new ArrayList<Address>();
        for(Address a : addresses)
        {
            lst.add(a);
        }
        change_addresses = lst;
    }
 
    public void setChangeAddressesKey(Iterable<ECKey> keys)
    {
        ArrayList<Address> lst = new ArrayList<Address>();
        for(ECKey key : keys)
        {
            lst.add(key.toAddress(params));
        }
        change_addresses = lst;
    }
    public void setProscribedAddresses(Iterable<Address> addresses)
    {
        HashSet<Address> set = new HashSet<Address>();
        for(Address a : addresses)
        {
            set.add(a);
        }
        proscribed_from_addresses =set;
        
    }


    public Transaction createSend(Address to_addr, BigInteger to_value)
        throws ScriptException
    {
        return createSend(to_addr, to_value, null);
    }
    public Transaction createSend(Address to_addr, BigInteger to_value, Collection<TransactionOutput> outs_to_use)
        throws ScriptException
    {
        HashMap<Address, BigInteger> to_map = new HashMap<Address, BigInteger>();
        to_map.put(to_addr, to_value);
        return createSend(to_map.entrySet(), outs_to_use);
    }

    public Transaction createSend(Collection<Map.Entry<Address,BigInteger>> to_pay)
        throws ScriptException
    {
        return createSend(to_pay, null);
    }

    public Transaction createSend(Collection<Map.Entry<Address,BigInteger>> to_pay, Collection<TransactionOutput> outs_to_use)
        throws ScriptException
    {

        LinkedList<TransactionOutput> things_to_spend=getThingsToSpend();

        Transaction tx = new Transaction(params);

        BigInteger sending = BigInteger.ZERO;
        for(Map.Entry<Address, BigInteger> me : to_pay)
        {
            Address addr = me.getKey();
            BigInteger val = me.getValue();
            tx.addOutput(val, addr);
            sending = sending.add(val);
        }

        HashMap<String, TransactionOutput> fund_source_map=new HashMap<String, TransactionOutput>();
       

        if (outs_to_use != null)
        {
            for(TransactionOutput out : outs_to_use)
            {
                fund_source_map.put(getString(out), out);
            }

        }

        consolidate(things_to_spend, fund_source_map);
        for(TransactionOutput out : fund_source_map.values())
        {
            tx.addInput(out);
        }

        BigInteger total = getFunds(fund_source_map).subtract(sending).subtract(fee_finder.getFeeForTx(tx, 1 + fund_source_map.size()));
        log.debug("total:  " + total);

        while(total.compareTo(BigInteger.ZERO) < 0)
        {
            log.debug("FundSourceMap: " + fund_source_map.toString());
            log.debug("Balance: " + getBalance(sending, fund_source_map));
            log.debug("Fee: " + fee_finder.getFeeForTx(tx, 1)); 
            if (things_to_spend.size() ==0) 
            {
                log.error("Insufficent funds");
                return null;
            }

            if (fund_source_map.size() >= inputs_before_we_get_serious)
            {
                things_to_spend = orderBySize(things_to_spend, total);    
            }
            TransactionOutput out = things_to_spend.pop();

            if (fund_source_map.put(getString(out), out)==null)
            { //Only adding this output if we don't have it already in the tx
                tx.addInput(out);
            }

            total = getFunds(fund_source_map).subtract(sending).subtract(fee_finder.getFeeForTx(tx, 1));

        }

        BigInteger funds=getFunds(fund_source_map);
        BigInteger fee = fee_finder.getFeeForTx(tx, 1);
        BigInteger change = funds.subtract(sending).subtract(fee);
        if (change.compareTo(BigInteger.ZERO) > 0)
        {

            if ((max_change_before_split!=null) && (change.compareTo(max_change_before_split) > 0))
            {
                fee = fee_finder.getFeeForTx(tx, 2);
                change = funds.subtract(sending).subtract(fee);
                BigInteger change_a = change.divide(BigInteger.valueOf(2));
                BigInteger change_b = change.subtract(change_a);

                tx.addOutput(change_a, getChangeAddress());
                tx.addOutput(change_b, getChangeAddress());
            }
            else
            {
            
                tx.addOutput(change, getChangeAddress());
            }
        }

        tx.signInputs(Transaction.SigHash.ALL, wallet);

        return tx;


        



        


    }


    /**
     * Create a send which sends all funds to the addresses in to_pay
     * which is a map of addresses to weights
     */
    public Transaction createSendAll(Map<Address,Double> to_pay)
        throws ScriptException
    {

        LinkedList<TransactionOutput> things_to_spend=getThingsToSpend();

        Transaction tx = new Transaction(params);

        BigInteger sending = BigInteger.ZERO;
        for(TransactionOutput out : things_to_spend)
        {
            sending = sending.add(out.getValue());
            tx.addInput(out);

        }
        BigInteger fee = fee_finder.getFeeForTx(tx, to_pay.size());
        BigInteger to_split = sending.subtract(fee);
        
        double total_weight=0;
        for(Double i : to_pay.values())
        {
            total_weight += i;
        }

        for(Map.Entry<Address, Double> me : to_pay.entrySet())
        {
            Address addr = me.getKey();
            double weight = me.getValue();
            double percent = weight / total_weight;
            long val = Math.round(Math.floor(to_split.doubleValue() * percent ));

            tx.addOutput(BigInteger.valueOf(val), addr);
        }

        tx.signInputs(Transaction.SigHash.ALL, wallet);

        return tx;


        



        


    }


    private Address getChangeAddress()
    {
        return change_addresses.get(rnd.nextInt(change_addresses.size()));
    }

    private BigInteger getBalance(BigInteger sending, Map<String, TransactionOutput> fund_source_map)
    {
        return getFunds(fund_source_map).subtract(sending);
    }


    private LinkedList<TransactionOutput> getThingsToSpend()
    {
        LinkedList<TransactionOutput> lst = new LinkedList<TransactionOutput>();

        Collection<TransactionOutput> output_collection;
        if ((wallet instanceof WalletDB) && (change_addresses != null) && (change_addresses.size() > 0))
        {
            WalletDB db = (WalletDB) wallet;
            output_collection = db.getUnspentOutputs(change_addresses);
        }
        else
        {
            output_collection = wallet.getUnspentOutputs();
        }

        for(TransactionOutput out : output_collection)
        {
            byte[] b=out.getScriptBytes();
            try
            {
                Address to_addr = out.getScriptPubKey().getToAddress();
                if ((proscribed_from_addresses == null) || (!proscribed_from_addresses.contains(to_addr)))
                {
                    lst.add(out);
                }
            }
            catch(ScriptException e)
            {
                log.warn("Script exception", e);

            }
        }
        return orderByDepth(lst);
    }

    private LinkedList<TransactionOutput> orderByDepth(LinkedList<TransactionOutput> input)
    {
        TreeMap<Double, TransactionOutput> map = new TreeMap<Double, TransactionOutput>();

        for(TransactionOutput out : input)
        {
            double depth=block_chain_cache.getDepth(out.getParent().getAppearsInHashes());
            if (depth > 5.0) depth=5.0;

            if (use_pending || (depth >= 1))
            {
                map.put(depth + rnd.nextDouble(), out);
            }
            
        }
        LinkedList<TransactionOutput> lst = new LinkedList<TransactionOutput>();

        while(map.size() > 0)
        {
            lst.add(map.pollLastEntry().getValue());
        }
        return lst;


    }

    private LinkedList<TransactionOutput> orderBySize(LinkedList<TransactionOutput> input, BigInteger target_amt)
    {
        TreeMap<Double, TransactionOutput> map = new TreeMap<Double, TransactionOutput>();

        LinkedList<TransactionOutput> lst = new LinkedList<TransactionOutput>();
    
        for(TransactionOutput out : input)
        {
            BigInteger diff = target_amt.subtract(out.getValue()).abs();
            double spot = rnd.nextDouble() + diff.doubleValue();
            map.put(spot, out);
        }


        while(map.size() > 0)
        {
            lst.add(map.pollLastEntry().getValue());
        }
        return lst;



    }

    private void consolidate(LinkedList<TransactionOutput> to_spend, Map<String, TransactionOutput> fund_source_map)
    {
        if (!consolidate_on_send) return;

        int added=0;
        for(TransactionOutput out : to_spend)
        {
            if (added >= consolidate_items) return;

            log.debug("Considering consolidation: " + out);
            if (out.getValue().compareTo(consolidate_below) <= 0)
            {
                log.debug("Looking at blocks...");
                if ((out.getParent().getAppearsInHashes() !=null) && (out.getParent().getAppearsInHashes().size() >0))
                {
                    log.debug("Adding to consolidation");
                    fund_source_map.put(getString(out), out);
                    added++;
                }
            }
        }
        
    }

    private BigInteger getFunds(Map<String, TransactionOutput> fund_source_map)
    {
        BigInteger total = BigInteger.ZERO;
        for(TransactionOutput out : fund_source_map.values())
        {
            total = total.add(out.getValue());
        }
        log.debug("Funds: " + total);
        return total;
    }

    public String getString(TransactionOutput m)
    {
        byte[] buff=new byte[20];
        rnd.nextBytes(buff);
        
        int idx=m.getParent().getOutputs().indexOf(m);

        return m.getParent().getHash().toString() +":" + idx;



    }
    
    public static BigInteger getBitIntFromBtc(double n)
    {
        double v=n*1e8;
        DecimalFormat df = new DecimalFormat("0");

        BigInteger bi = new BigInteger(df.format(v));
        return bi;
    }

}
